import React, {
  DragEventHandler,
  KeyboardEventHandler,
  useCallback,
  useEffect,
  useImperativeHandle,
  useMemo,
  useRef,
  useState,
} from 'react';

import { Spin } from 'antd';
import { createEditor, Editor, Operation, Point, Range, Transforms } from 'slate';
import { HistoryEditor, withHistory } from 'slate-history';
import { ReactEditor, RenderLeafProps, withReact } from 'slate-react';

import { EditableWrap, Leaf, SlateElement, SlateWrap, withInput, withTag, Toolbar } from './components';
import { useMountedEffect, useOnSlateItemClick, useShareDocEditorData } from './hooks';
import Style from './main.module.scss';
import { DocEditorInstance, DocEditorProps, ShareDocEditorData, SlateDescendant, SlateElementProps } from './types';
import {
  createClassNameMark,
  emitEditorMouseUp,
  emitToolbarRefresh,
  isValidStringKeyCode,
  parseToDocData,
  parseToSlateData,
} from './utils';
import { 
  checkDisableClearV2, 
  checkDisableEditor,
  checkDisableInput,
  checkDisableNewline,
} from '../office-lib/word-editor/utils';
import SlateEditorContext from './SlateEditorContext';

export const DocEditorToolbar = Toolbar;
const classNames = createClassNameMark(Style);
const defaultData: DocEditorProps['data'] = {};

/**
 * 文档编辑器
 */
export const DocEditor = React.memo(
  React.forwardRef(function DocEditor(
    {
      value,
      loading = false,
      data = defaultData,
      onDocItemClick,
      onChange: onDocItemsChange,
      onSlateInputDataChanged
    }: DocEditorProps,
    ref: React.ForwardedRef<DocEditorInstance>,
  ) {
    const [isRefresh, setIsRefresh] = useState(false);
    const [selectionPosition, setSelectionPosition] = useState<[Point, Point] | null>(null);
    const [dataState, setDataState] = useShareDocEditorData();
    const dataStateRef = useRef(dataState);
    const docItemsRef = useRef(value);
    // const editor = useMemo(() => withTag(withInput(withHistory(withReact(createEditor())))), []) as ReactEditor & HistoryEditor;
    const editor = useMemo(() => withTag(withInput(withReact(createEditor()))), []) as ReactEditor & HistoryEditor;

    const [slateData, state] = useMemo(() => parseToSlateData(value, data), [data, value]);

    const renderElement = useCallback((props: SlateElementProps) => <SlateElement {...props} />, []);
    const renderLeaf = useCallback((props: RenderLeafProps) => <Leaf {...props} />, []);
    const undoInfoRef = useRef<{ length: number } | null>(null);
    const emitDocItemsChange = useCallback(
      (val: SlateDescendant[]) => {
        const data = parseToDocData(val, dataStateRef.current, docItemsRef.current);
        docItemsRef.current = data;
        onDocItemsChange?.(data);
      },
      [onDocItemsChange],
    );
    const onChange = useCallback(
      (val: SlateDescendant[]) => {
        const { selection } = editor;
        const changeOperations = editor.operations.filter(({ type }) => 'set_selection' !== type);
        setSelectionPosition(selection ? Range.edges(selection) : null);
        emitToolbarRefresh();
        changeOperations.length && emitDocItemsChange(val);
      },
      [editor, emitDocItemsChange],
    );
    const onKeyDown: KeyboardEventHandler = useCallback(
      (e) => {
        checkDisableEditor(editor, selectionPosition) && e.preventDefault();
        switch (e.key) {
          case 'Backspace':
            checkDisableClearV2(editor, selectionPosition) && e.preventDefault();
            break;
          case 'Enter':
            checkDisableNewline(editor, selectionPosition) && e.preventDefault();
            break;
          case 'x':
          case 'X':
            e.ctrlKey &&
              !e.shiftKey &&
              !e.altKey &&
              checkDisableClearV2(editor, selectionPosition) &&
              e.preventDefault();
            break;
          case 'v':
          case 'V':
            e.ctrlKey &&
              !e.shiftKey &&
              !e.altKey &&
              checkDisableInput(editor.children, selectionPosition) &&
              e.preventDefault();
            break;
          default:
            !undoInfoRef.current &&
              !e.ctrlKey &&
              isValidStringKeyCode(e.keyCode) &&
              checkDisableInput(editor.children, selectionPosition) &&
              e.preventDefault();
        }
      },
      [editor.children, selectionPosition],
    );
    const onDragStart: DragEventHandler = useCallback((e) => {
      e.preventDefault();
    }, []);
    const onCompositionStart = useCallback(() => {
      const { children, history } = editor;
      if (!checkDisableInput(children, selectionPosition, true) || undoInfoRef.current) return;
      undoInfoRef.current = { length: history.undos.length };
    }, [editor, selectionPosition]);
    const onCompositionEnd = useCallback(() => {
      if (!undoInfoRef.current) return;
      const { length } = undoInfoRef.current;
      setTimeout(() => {
        const { history } = editor;
        const { undos } = history;
        const batchItems = undos.slice(length).reverse();
        HistoryEditor.withoutSaving(editor, () => {
          Editor.withoutNormalizing(editor, () => {
            const biLength = batchItems.length;
            batchItems.forEach((batch, index) => {
              const inverseOps = batch.operations.map(Operation.inverse).reverse();
              for (const op of inverseOps) editor.apply(op);
              index === biLength - 1 && batch.selectionBefore && Transforms.setSelection(editor, batch.selectionBefore);
            });
          });
        });
        history.undos.length = length;
      });
      undoInfoRef.current = null;
    }, [editor]);
    const onSlateItemClick = useCallback(
      (element: SlateElementProps['element']) => {
        if (!onDocItemClick) return;
        const eId = element.id;
        const docItem = docItemsRef.current.find(({ id }) => id === eId);
        const elements = document.querySelectorAll(`[data-doc-item-id='${eId}']`);
        docItem && onDocItemClick(docItem, elements);
      },
      [onDocItemClick],
    );
    useOnSlateItemClick(onSlateItemClick);
    useEffect(() => {
      setDataState(state);
    }, [setDataState, state]);
    useEffect(() => {
      dataStateRef.current = dataState;
    }, [dataState]);
    useEffect(() => {
      emitDocItemsChange(editor.children);
    }, [editor.children, emitDocItemsChange]);
    useMountedEffect(() => {
      setIsRefresh(true);
      Promise.resolve().then(() => {
        setIsRefresh(false);
      });
    }, [slateData]);

    useImperativeHandle(
      ref,
      () => ({
        editor,
        forceUpdate,
        forceUpdateV2,
        getSlateData,
        getSlateEditor
      }),
      [editor],
    );

    const getSlateEditor = () => {
      return editor;
    }

    const forceUpdate = () => {
      setIsRefresh(true);
      Promise.resolve().then(() => {
        setIsRefresh(false);
      });
    }

    const forceUpdateV2 = () => {
      setIsRefresh(true);
      setTimeout(() => {
        setIsRefresh(false);
      }, 120);
    }

    const getSlateData = () => {
      return slateData;
    }

    const _onSlateInputDataChanged = (e) => {
      onSlateInputDataChanged && onSlateInputDataChanged(e);
    }

    const handleDragOver = (event) => {
      event.preventDefault();
    };

    const handleDrop = (event) => {
      event.preventDefault();
    };

    return isRefresh || loading ? (
      <div className={classNames('doc-editor--loading')}>
        <Spin />
      </div>
    ) : (
      <SlateEditorContext.Provider
        value={{
          onSlateInputDataChanged: _onSlateInputDataChanged
        }}
      >
        <SlateWrap
          editor={editor}
          initialValue={slateData}
          onChange={onChange}
        >
          <EditableWrap
            className={classNames('doc-editor')}
            renderElement={renderElement}
            renderLeaf={renderLeaf}
            onKeyDown={onKeyDown}
            onDragStart={onDragStart}
            onCompositionStart={onCompositionStart}
            onCompositionEnd={onCompositionEnd}
            onDragOver={handleDragOver}
            onDrop={handleDrop}
            onMouseUp={emitEditorMouseUp}
          />
        </SlateWrap>
      </SlateEditorContext.Provider>
    );
  }),
);
